/**
 * Software License, Version 1.0
 * 
 * Copyright 2003 The Trustees of Indiana University.  All rights reserved.
 * 
 *
 *Redistribution and use in source and binary forms, with or without 
 *modification, are permitted provided that the following conditions are met:
 *
 *1) All redistributions of source code must retain the above copyright notice,
 * the list of authors in the original source code, this list of conditions and
 * the disclaimer listed in this license;
 *2) All redistributions in binary form must reproduce the above copyright 
 * notice, this list of conditions and the disclaimer listed in this license in
 * the documentation and/or other materials provided with the distribution;
 *3) Any documentation included with all redistributions must include the 
 * following acknowledgement:
 *
 *"This product includes software developed by the Community Grids Lab. For 
 * further information contact the Community Grids Lab at 
 * http://communitygrids.iu.edu/."
 *
 * Alternatively, this acknowledgement may appear in the software itself, and 
 * wherever such third-party acknowledgments normally appear.
 * 
 *4) The name Indiana University or Community Grids Lab or NaradaBrokering, 
 * shall not be used to endorse or promote products derived from this software 
 * without prior written permission from Indiana University.  For written 
 * permission, please contact the Advanced Research and Technology Institute 
 * ("ARTI") at 351 West 10th Street, Indianapolis, Indiana 46202.
 *5) Products derived from this software may not be called NaradaBrokering, 
 * nor may Indiana University or Community Grids Lab or NaradaBrokering appear
 * in their name, without prior written permission of ARTI.
 * 
 *
 * Indiana University provides no reassurances that the source code provided 
 * does not infringe the patent or any other intellectual property rights of 
 * any other entity.  Indiana University disclaims any liability to any 
 * recipient for claims brought by any other entity based on infringement of 
 * intellectual property rights or otherwise.  
 *
 *LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH NO 
 *WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA UNIVERSITY GIVES
 *NO WARRANTIES AND MAKES NO REPRESENTATION THAT SOFTWARE IS FREE OF 
 *INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR OTHER PROPRIETARY RIGHTS. 
 *INDIANA UNIVERSITY MAKES NO WARRANTIES THAT SOFTWARE IS FREE FROM "BUGS", 
 *"VIRUSES", "TROJAN HORSES", "TRAP DOORS", "WORMS", OR OTHER HARMFUL CODE.  
 *LICENSEE ASSUMES THE ENTIRE RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR 
 *ASSOCIATED MATERIALS, AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION 
 *GENERATED USING SOFTWARE.
 */
package cgl.narada.protocol;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Random;

import cgl.narada.event.NBEvent;
import cgl.narada.util.ByteUtilities;

/**
 * This class is responsible for dealing with clients which are connected to a
 * server node. Specifically this class is responsible for
 * <ul>
 * <li>The generation of client addresses (which include the servers address
 * too).
 * <li> Deal with requests for interest in subscriptions
 * <li> Route relevant events to the clients, based on their profiles.
 * <ul>
 * 
 * @author Shrideep Pallickara 
 * $Revision$
 * $Date$
 */

public class ClientConnectionHandler implements ProtocolDebugFlags {
    /**
     * The number of clients presently attached to the server. Reflects
     * concurrent connections maintained by the broker
     */
    private int numOfClients = 0;

    /** The id's being assigned to clients */
    private int clientId = 1000;

    /** A reference to the protocol Handler class */
    private ProtocolHandler protocolHandler;

    /** The destinationID of the node in question */
    private Destinations thisDestination;

    /** The system Level of the system */
    private int systemLevel;

    /**
     * The vertex Node for the node in question. Helpful in the construction of
     * gateways
     */
    private Gateway vertexNode;

    private Hashtable clientConnections;

    private int connectionLimit;

    private String moduleName = "ClientConnectionHandler: ";

    /**
     * Constructor for the Client connection handler
     * 
     * @param protocolHandler
     *            Reference to the protocolHandler class
     */
    public ClientConnectionHandler(ProtocolHandler protocolHandler,
            int connectionLimit) {
        this.protocolHandler = protocolHandler;
        this.connectionLimit = connectionLimit;
        clientConnections = new Hashtable();
        Random random = new Random();
        clientId = Math.abs(random.nextInt());
        EntityEventDispatcher.getInstance().initialize(this);
    }

    /**
     * The invocation of this method implies that the node in question has been
     * assigned a node address by the system, and also that this node is ready
     * to process connection requests from clients and assign them addresses.
     * 
     * @param destination
     *            The destination for the node in question
     */
    public void setNodeDestination(Destinations destination) {
        thisDestination = destination;
        systemLevel = thisDestination.getSystemLevel();
    }

    /**
     * This method sets the vertex node for the node in question. We need this
     * since we map the unassigned nodes to gateways within the transport layer.
     * We thus need a handle to the vertexNode when we are re-mapping the client
     * connections as gateways at level (-1).
     * 
     * @param vertexNode
     *            The vertex Node of the connectivity graph at this node.
     */
    public void setVertexNode(Gateway vertexNode) {
        this.vertexNode = vertexNode;
    }

    /**
     * Returns the number of active concurrent connections
     * 
     * @return Number of active connections
     */
    public int getConcurrentConnections() {
        return numOfClients;
    }

    /** Returns the limit on the active concurrent connections */
    public int getConcurrentConnectionsLimit() {
        return connectionLimit;
    }

    public void processClientRegistration(byte[] registerBytes,
            String clientNode) {
        ByteArrayInputStream baInputStream = new ByteArrayInputStream(
                registerBytes);
        DataInputStream din = new DataInputStream(baInputStream);

        try {
            din.readByte();
            /** Protocol.DATA */
            din.readByte();
            /** Protocol.NODE_ADDRESS */

            int indicator = din.readByte();
            /** Protocol.CLIENT_SETUP */
            if (indicator == Protocol.CLIENT_SETUP) {
                int clientId = din.readInt();
                registerClient(clientId, clientNode);
            } else {
                System.out.println(moduleName
                        + "Problem with client registration");
            }
        } catch (IOException ioe) {
            System.out.println(moduleName + "Error marshalling stream" + ioe);
        } /* end try-catch */

    }

    private void registerClient(int clientId, String clientNode) {
        Object clientIdentifier = new Integer(clientId);

        byte[] destinationBytes = new byte[4];
        destinationBytes = ByteUtilities.getBytes(clientId);

        Gateway clientGateway = new Gateway(vertexNode, new NodeAddress(
                destinationBytes, true), (short) -1);
        clientConnections.put(clientIdentifier, clientGateway);
        protocolHandler.mapLinkToClientNode(clientNode, clientGateway,
                clientIdentifier);

        ByteArrayOutputStream baOutputStream = new ByteArrayOutputStream();
        DataOutputStream dout = new DataOutputStream(baOutputStream);

        try {
            dout.writeByte(Protocol.DATA);
            dout.writeByte(Protocol.NODE_ADDRESS);
            dout.writeByte(Protocol.CLIENT_SETUP_SUCCESS);
        } catch (IOException ioe) {
            System.out.println(moduleName + "Error marshalling stream" + ioe);
        } /* end try-catch */

        protocolHandler.sendToNode(clientGateway, baOutputStream.toByteArray());
        System.out.println(moduleName + "Client [" + clientId
                + "] @ " + clientNode + " registered");
    }

    // HG: For routing error (no access to specified topic) to the client
    
    /**
     * Sends the message (msg) to the appropriate client identified by the
     * clientId and clientNode combination
     * 
     * @param clientId
     * @param clientNode
     * @param msg
     */
    public void sendMessageToClient(int clientId, String clientNode, byte[] msg) {
        Object clientIdentifier = new Integer(clientId);

        byte[] destinationBytes = new byte[4];
        destinationBytes = ByteUtilities.getBytes(clientId);

        Gateway clientGateway = new Gateway(vertexNode, new NodeAddress(
                destinationBytes, true), (short) -1);
        clientConnections.put(clientIdentifier, clientGateway);
        protocolHandler.mapLinkToClientNode(clientNode, clientGateway,
                clientIdentifier);

        ByteArrayOutputStream baOutputStream = new ByteArrayOutputStream();
        DataOutputStream dout = new DataOutputStream(baOutputStream);

        try {
            dout.writeByte(Protocol.DATA);
            dout.write(msg);
        } catch (IOException ioe) {
            System.out.println(moduleName + "Error marshalling stream" + ioe);
        } /* end try-catch */

        protocolHandler.sendToNode(clientGateway, baOutputStream.toByteArray());
    }

    // ---
    
    /**
     * This method is responsible for handling client address requests and the
     * creation of responses to these clients in response to their address
     * requests
     * 
     * @param data
     *            The address request format.
     */
    public void processClientAddressRequest(String clientNode, byte[] data) {
        byte[] addressResponse;
        if (thisDestination == null) {
            addressResponse = new byte[1];
            addressResponse[0] = Protocol.NAP_CLIENT_ADD_RESPONSE;
            protocolHandler.sendTo(clientNode, addressResponse);
            return;
        }
        /* Increment the active concurrent connection count */
        numOfClients++;

        /** Generate unique ID */
        clientId++;

        addressResponse = new byte[1 /* The protocol header */+ 4 /*
                                                                     * The
                                                                     * destination
                                                                     * for the
                                                                     * client
                                                                     */+ 4 * (systemLevel + 1)];
        int offset = 0;
        addressResponse[offset++] = Protocol.NAP_CLIENT_ADD_RESPONSE;
        /** Marshall the destination ID assigned by the system */
        byte[] destinationBytes = new byte[4];
        destinationBytes = ByteUtilities.getBytes(clientId);
        System.arraycopy(destinationBytes, 0, addressResponse, offset, 4);
        offset += 4;

        /**
         * Marshall the destination ID associated with the server node in
         * question
         */
        System.arraycopy(thisDestination.prepareDestinationsToSendOverLevel(0),
                0, addressResponse, offset, 4 * (systemLevel + 1));
        Gateway clientGateway = new Gateway(vertexNode, new NodeAddress(
                destinationBytes, true), (short) -1);

        if (ClientConnectionHandler_Debug) {
            System.out.println("ClientId=" + clientId + " ,Client Node="
                    + clientNode + "\n");
        }
        Object clientIdentifier = new Integer(clientId);
        protocolHandler.mapLinkToClientNode(clientNode, clientGateway,
                clientIdentifier);
        protocolHandler.sendToNode(clientGateway, addressResponse);
        clientConnections.put(clientIdentifier, clientGateway);
    }

    /**
     * This method provides for routing information to clients that are
     * connected to the server node. Given a list of clients to which data needs
     * to be routed to, this method routes data to each client conatined within
     * the encapsulated destination list.
     * 
     * @param data
     *            The data that needs to be routed.
     * @param destinations
     *            The list of clients to which this information needs to be
     *            routed.
     */
    public void sendToClientNodes(byte[] data, Hashtable destinations) {
        if (ClientConnectionHandler_Debug) {
            System.out.print(moduleName + "Will route to [");
            Enumeration _keys = destinations.keys();
            while (_keys.hasMoreElements()) {
                System.out.print(" " + _keys.nextElement());
            }
            System.out.print("] \n");
        }

        Enumeration keys = destinations.keys();
        while (keys.hasMoreElements()) {
            Integer clientKey = (Integer) keys.nextElement();            
            if (ClientConnectionHandler_Debug) {
                // System.out.println("To route to " + clientKey);
            }

            if (clientConnections.containsKey(clientKey)) {
                Gateway _gateway = (Gateway) clientConnections.get(clientKey);
                protocolHandler.sendToNode(_gateway, data);
            } else {
                System.out
                        .println("ClientConnectionHandler :: Unknown Destination"
                                + clientKey);
            }/* end if(containsKey) */
        }/* end while */
        
        
    }

    public void sendToClientNodes(byte[] data, Hashtable destinations,
            int ignoreDestination) {
        if (ClientConnectionHandler_Debug) {
            System.out.print(moduleName + "Will route to [");
            Enumeration _keys = destinations.keys();
            while (_keys.hasMoreElements()) {
                System.out.print(" " + _keys.nextElement());
            }
            System.out.print("] \n");
        }

        Enumeration keys = destinations.keys();
        Integer ignoreKey = new Integer(ignoreDestination);
        while (keys.hasMoreElements()) {
            Integer clientKey = (Integer) keys.nextElement();
            if (clientKey.equals(ignoreKey)) {
                continue;
            }
            /** Ignore the specified destination */
            // System.out.println("To route to " + clientKey);
            if (clientConnections.containsKey(clientKey)) {
                Gateway _gateway = (Gateway) clientConnections.get(clientKey);
                protocolHandler.sendToNode(_gateway, data);
            } else {
                System.out
                        .println("ClientConnectionHandler :: Unknown Destination"
                                + clientKey);
            }/* end if(containsKey) */
        }/* end while */
    }

    /**
     * This method provides for routing information to clients that are
     * connected to the server node. Given a list of clients to which data needs
     * to be routed to, this method routes data to each client conatined within
     * the encapsulated destination list.
     * 
     * @param data
     *            The data that needs to be routed.
     * @param destinations
     *            The list of clients to which this information needs to be
     *            routed.
     */
    public void sendToClientNodes(byte[] data, ArrayList destinations,
            int ignoreDestination) {
        Integer ignoreKey = new Integer(ignoreDestination);
        if (ClientConnectionHandler_Debug) {
            System.out.println(moduleName + "Will route to " + destinations);
            System.out.println(moduleName + "Need to ignore destination "
                    + ignoreKey);
        }

        // route to all clients
        for (int i = 0; i < destinations.size(); i++) {
            Integer clientKey = (Integer) destinations.get(i);
            if (clientKey.equals(ignoreKey)) {
                continue;
            }
            /** Ignore the specified destination */

            if (clientConnections.containsKey(clientKey)) {
                Gateway _gateway = (Gateway) clientConnections.get(clientKey);
                protocolHandler.sendToNode(_gateway, data);
            } else {
                // System.out.println(moduleName + "UNKNOWN DESTINATION!!" +
                // clientKey);
            }/* end if(containsKey) */
        }
    }

    /**
     * This method provides for routing information for one client connected to
     * the server node. This method routes data to each the given client
     * 
     * @param data
     *            The data that needs to be routed.
     * @param destination
     *            The clientID of the client to which this information needs to
     *            be routed.
     */
    public void sendToClientNode(byte[] data, int destination) {

        if (ClientConnectionHandler_Debug) {
            System.out.println(moduleName + "Will route to " + destination);
        }

        Integer clientKey = new Integer(destination);
        if (clientConnections.containsKey(clientKey)) {
            Gateway _gateway = (Gateway) clientConnections.get(clientKey);
            protocolHandler.sendToNode(_gateway, data);
        } else {
            // System.out.println(moduleName + "UNKNOWN DESTINATION!!" +
            // clientKey);
        }

    }

    public void routePayloadToEntity(Object clientKey, byte[] data) {
        if (clientConnections.containsKey(clientKey)) {
            Gateway _gateway = (Gateway) clientConnections.get(clientKey);
            protocolHandler.sendToNode(_gateway, data);
        } else {
            System.out
                    .println(moduleName + "UNKNOWN DESTINATION!!" + clientKey);
        }// end if(containsKey)
    }

    /** Testing the application */
    public static void main(String[] args) {
        ClientConnectionHandler ccl = new ClientConnectionHandler(
                new ProtocolHandler(), 3000);
        ccl.setNodeDestination(new Destinations(3));
        byte[] data = new byte[4];

        // for (int i=0; i < 5; i++)
        // ccl.processClientAddressRequest("shri", data);
    }
}
